Skip to content

feat(discord): include reply/quote context in agent prompt (#339)#527

Closed
ChunHao-dev wants to merge 3 commits intoopenabdev:mainfrom
ChunHao-dev:feat/discord-reply-context
Closed

feat(discord): include reply/quote context in agent prompt (#339)#527
ChunHao-dev wants to merge 3 commits intoopenabdev:mainfrom
ChunHao-dev:feat/discord-reply-context

Conversation

@ChunHao-dev
Copy link
Copy Markdown
Contributor

Summary

When a user replies to (quotes) a message in a Discord thread, the bot only sends the new message text to the agent. The quoted/referenced message content is lost — the agent has no idea what "this" refers to.

This PR reads msg.referenced_message and prepends the quoted content to the prompt:

[Quoted message from @username]:
<content of the quoted message>

summarize this

Implementation

  • resolve_referenced_message() — prefers gateway-provided referenced_message (zero cost); falls back to HTTP API call via message_reference if the gateway didn't include the full message
  • format_quote_context() — pure function that formats the quote block
  • Injected after resolve_mentions(), before the prompt is sent to the router
  • Non-reply messages are completely unaffected (resolve_referenced_message returns None)

Testing

  • 4 unit tests for format_quote_context() (normal, empty content, empty prompt, multiline)
  • Manual testing on OrbStack K8s deployment
  • All 109 existing tests pass

Closes #339

Discord Discussion URL: https://discord.com/channels/1491295327620169908/1496538680142069800

@ChunHao-dev ChunHao-dev requested a review from thepagent as a code owner April 22, 2026 15:52
@github-actions github-actions Bot added the pending-screening PR awaiting automated screening label Apr 22, 2026
@shaun-agent
Copy link
Copy Markdown
Contributor

OpenAB PR Screening

This is auto-generated by the OpenAB project-screening flow for context collection and reviewer handoff.
Click 👍 if you find this useful. Human review will be done within 24 hours. We appreciate your support and contribution 🙏

Screening report ## Intent

This PR fixes a Discord conversation-context gap: when a user replies to or quotes an earlier message in a thread, the agent currently only sees the new message text and loses the referenced message content. That makes prompts like “summarize this” or “answer that” ambiguous and degrades response quality for Discord users.

Feat

This is a feature-sized fix to Discord prompt assembly. It adds reply/quote context to the agent prompt by resolving the referenced Discord message, formatting it into a quote block, and prepending it to the user’s new message before routing to the agent. Non-reply messages remain unchanged.

Who It Serves

The primary beneficiary is Discord end users interacting with OpenAB agents in threads and reply chains. Secondarily, it helps maintainers and reviewers by making agent behavior more predictable and aligned with how users naturally converse in Discord.

Rewritten Prompt

Update the Discord adapter so reply messages include the referenced message content in the prompt sent to the agent.

Requirements:

  • Detect when an incoming Discord message references another message.
  • Resolve the referenced message by preferring gateway-provided referenced_message data and falling back to a Discord API fetch only when necessary.
  • Format the referenced content as a clearly labeled quote block including the original author handle.
  • Prepend that quote block to the current user prompt after mention resolution and before router dispatch.
  • Preserve current behavior for non-reply messages.
  • Add unit tests for formatting and resolution edge cases, including empty quoted content, multiline content, and missing referenced payloads.

Merge Pitch

This is worth moving forward because it fixes a real prompt-quality issue in a core user interaction path without changing the broader routing model. The risk profile is low to moderate: behavior is isolated to Discord reply handling, but reviewers will likely want to confirm prompt formatting is stable, API fallback is safe, and quoted context does not introduce noisy or misleading prompt injection in edge cases.

Best-Practice Comparison

OpenClaw principles:

  • Relevant: explicit delivery context is relevant here. Passing quoted content into the prompt is a lightweight form of better delivery routing because the agent receives the conversational state needed to interpret the message correctly.
  • Somewhat relevant: retry/backoff matters only for the fallback API fetch path if referenced message resolution depends on an HTTP call.
  • Not especially relevant: gateway-owned scheduling, durable job persistence, isolated executions, and run logs are broader execution-system concerns and do not materially affect this PR’s narrow prompt-enrichment scope.

Hermes Agent principles:

  • Relevant: self-contained prompts for scheduled tasks maps well to this PR’s core idea. The prompt should contain enough local context to stand on its own, and reply context improves that.
  • Somewhat relevant: fresh-session thinking is indirectly aligned, because when memory is thin or absent, embedding the quoted message in the prompt makes each turn more self-sufficient.
  • Not relevant: gateway tick model, file locking, and atomic persisted writes do not fit this feature because no scheduler or persisted state is being introduced.

Overall:

  • The strongest best-practice alignment is with “self-contained inputs” and “explicit context delivery.”
  • This PR does not need scheduler-grade durability or persistence patterns unless reply resolution is later expanded into a more complex message-state subsystem.

Implementation Options

  1. Conservative option: inline quoted text only
  • Use referenced_message when present.
  • If missing, skip quote context instead of performing a fallback fetch.
  • Keep the implementation fully gateway-bound and low-risk.
  1. Balanced option: gateway-first with HTTP fallback
  • Prefer referenced_message.
  • Fallback to message_reference API fetch when the gateway payload is incomplete.
  • Format the quote consistently and inject it before routing.
  • Add focused tests around formatting and fallback behavior.
  1. Ambitious option: structured conversation-context builder
  • Build a reusable prompt-context layer for Discord that can include quoted messages, attachments, embeds, author metadata, and possibly limited parent-thread context.
  • Centralize prompt assembly rules instead of adding a single reply-specific branch.
  • Prepare the adapter for future multi-message context enrichment.

Comparison Table

Option Speed to ship Complexity Reliability Maintainability User impact Fit for OpenAB right now
Conservative: inline quoted text only Fast Low Medium High Medium Good
Balanced: gateway-first with HTTP fallback Medium-fast Medium High High High Best
Ambitious: structured context builder Slow High Medium-high Medium Very high Premature

Recommendation

The balanced option is the right path for merge discussion. It solves the actual user problem reliably, keeps the change scoped to the Discord adapter, and avoids overbuilding a general context system before the project has validated the need for broader prompt-enrichment rules.

If this moves forward, the likely follow-up split is:

  1. Merge reply/quote context support now.
  2. Later evaluate whether attachments, embeds, or thread-parent context deserve the same treatment through a shared context-builder abstraction.

@CHC-Agent
Copy link
Copy Markdown
Contributor

Fix: resolve mentions in quoted message content

The quoted message content from referenced_message was being injected into the prompt without running resolve_mentions(). This meant raw Discord mention markup (<@BOT_ID>, <@&ROLE_ID>) could leak into the LLM prompt — and if the LLM echoed them back, Discord would actually ping those roles/users.

Change: Added resolve_mentions(&quoted.content, bot_id) before passing quoted content to format_quote_context(), consistent with how the user's own message is already processed.

Some(quoted) => {
    let quoted_content = resolve_mentions(&quoted.content, bot_id);
    format_quote_context(&quoted.author.name, &quoted_content, &prompt)
}

Minimal change — 4 lines added, 1 removed.

masami-agent
masami-agent previously approved these changes Apr 30, 2026
Copy link
Copy Markdown
Contributor

@masami-agent masami-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: #527

Summary

  • Problem: When a user replies to (quotes) a message in a Discord thread, the quoted content is lost — the agent only sees the new message text with no context about what "this" refers to.
  • Approach: Read msg.referenced_message (gateway-provided, zero cost) with HTTP API fallback, prepend formatted quote block to the prompt before sending to the ACP agent.
  • Risk level: Low

Core Assessment

  1. Problem clearly stated: ✅ — well-documented in both issue #339 and PR description
  2. Approach appropriate: ✅ — two-tier resolution (gateway cache → HTTP fallback) is the correct pattern for serenity 0.12
  3. Alternatives considered: ✅ — the gateway-first + HTTP-fallback design is explicitly documented
  4. Best approach for now: ✅ — minimal, focused, non-breaking

Findings

Code correctness:

  • resolve_referenced_message() correctly handles serenity 0.12 types: message_reference.channel_id is ChannelId (non-optional), message_id is Option<MessageId> — the ? operator usage is correct.
  • *referenced.clone() dereferences the Box<Message> — correct pattern for Option<Box<Message>>.
  • resolve_mentions() is applied to the quoted content, which correctly strips bot mentions from the quoted text too. Good attention to detail.
  • Insertion point is correct: after resolve_mentions() on the user's own message, before the empty-check gate. This means a reply with empty user text but non-empty quoted content will still be processed — which is the right behavior.
  • The tracing::warn! on HTTP fetch failure with structured fields follows the project's existing logging pattern.

format_quote_context() design:

  • Pure function, easy to test — good separation.
  • Empty quoted_content returns prompt unchanged — correct guard.
  • The format [Quoted message from @{author_name}]:\n{content}\n\n{prompt} is clean and gives the agent clear context about who said what.

Review Summary

🔧 Suggested Changes

  • Consider adding a length cap on quoted content. If someone quotes a very long message (e.g., a full code dump from the bot), the entire thing gets prepended to the prompt. A reasonable truncation (e.g., first 2000 chars with a [truncated] marker) would prevent unexpectedly large prompts. Not blocking — this can be a follow-up.

ℹ️ Info

  • This only handles single-level quoting (the direct referenced_message). Nested quotes (quoting a message that itself was a reply) won't include the deeper context. This is fine for now — Discord's own UI only shows one level of reply context.
  • The HTTP fallback path (channel_id.message(http, message_id)) counts against Discord's rate limit. In practice this should be rare since the gateway almost always includes referenced_message, but worth noting for awareness.

⚪ Nits

  • None — code is clean and well-structured.

Verdict

APPROVE — Clean, focused implementation. Single file changed, 4 unit tests, all 109 existing tests pass, CI green across all 7 smoke-test variants. The code correctly handles serenity 0.12 types, follows existing project patterns, and the insertion point is well-chosen. Ready for maintainer review.

obrutjack
obrutjack previously approved these changes Apr 30, 2026
Copy link
Copy Markdown
Collaborator

@obrutjack obrutjack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Clean, focused implementation — gateway-first with HTTP fallback, resolve_mentions applied to quoted content, good test coverage. LGTM.

Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Conditional Approve — Needs rebase, feature is net-new and valuable.

Baseline Check (Step 0)
Field Value
State OPEN
Mergeable CONFLICTING
Created 2026-04-22
Labels pending-screening
Changes reply/quote context injection into agent prompt

Main branch status: No existing reply/quote handling. Feature is 100% net-new. Conflict is from surrounding discord.rs changes (PR #666, #646), not from overlapping features.

Four-Question Framework

1. What problem does it solve?
When a user replies to (quotes) a message in a Discord thread, the bot only sends the new message text — the quoted content is lost. The agent has no idea what "this" refers to.

2. How does it solve it?

  • resolve_referenced_message() — prefers gateway-provided referenced_message (zero cost); falls back to HTTP API call via message_reference
  • format_quote_context() — pure function that formats the quote block
  • Injected after resolve_mentions(), before prompt is sent to router
  • Non-reply messages completely unaffected

3. What was considered?
Clean separation of async I/O (resolve) and pure formatting (format). Graceful degradation on HTTP fallback failure.

4. Is it the best approach?
Yes — the two-layer resolution (gateway payload → HTTP fallback) is the right pattern. The pure formatting function is well-tested.

Traffic Light

🟢 INFO

  • Clean separation of async I/O and pure formatting
  • Graceful degradation: HTTP fallback failure logs warning, continues without quote
  • resolve_mentions() applied to quoted content too
  • 4 unit tests covering edge cases
  • Zero impact on non-reply messages
  • Excellent PR description

🟡 NIT

  1. Content length cap: Long quoted messages (4000+ chars) prepended in full. Consider truncating to ~500-1000 chars with [truncated] marker to prevent prompt bloat.
  2. Attachment-only quotes: Quoted messages with no text but attachments silently pass through. A follow-up could add [contained attachments only] note.

🔴 SUGGESTED CHANGES

  1. Must rebase — PR has merge conflicts with main. ~20 commits landed on main since PR opened. Contributor needs to rebase and resolve conflicts.

Reviewed by 超渡法師 🔃 chaodu Backlog triage

Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Conditional Approve — Clean feature, MUST rebase first

📋 Baseline Check (Step 0)

Checked main branch first: no reply/quote handling exists. This feature is 100% net-new. The PR adds resolve_referenced_message() and format_quote_context() right after resolve_mentions() in the message handler. No overlap or regression risk with existing code.

🧭 Four-Question Framework

1. What problem does this solve?

When users reply to a message in Discord, the bot currently has no idea what message is being referenced. This PR injects the quoted message content into the agent prompt so the bot can understand conversational context from replies.

2. How does it solve it?

Two new functions in src/discord.rs (+72 lines):

  • resolve_referenced_message() — async I/O to fetch the referenced message (tries cache first, falls back to HTTP)
  • format_quote_context() — pure formatting function that prepends > @author: content to the prompt

Inserted right after resolve_mentions() in the message handler pipeline.

3. What was considered?

  • Correct serenity 0.12 types: Box<Message> deref, message_reference.channel_id is ChannelId, message_id is Option<MessageId>
  • resolve_mentions() is applied to quoted content too — good attention to detail
  • Graceful degradation: if HTTP fallback fails, the message is processed without quote context (no crash)
  • 4 unit tests covering edge cases

4. Is this the best approach?

The separation of async I/O and pure formatting is clean and testable. The approach is solid. A few improvements to consider (see traffic light below).

🚦 Traffic Light

🟢 INFO — Clean function separation (async I/O vs pure formatting) makes this easy to test and maintain.

🟢 INFO — Applying resolve_mentions() to quoted content prevents raw <@UID> leaking into the prompt. Nice catch.

🟢 INFO — 4 unit tests with good edge case coverage.

🔴 SUGGESTED CHANGES — PR is in CONFLICTING state. Many PRs have merged since April 22. Please rebase against main and resolve conflicts before this can be merged.

🟡 NIT — No length cap on quoted content. A very long quoted message could bloat the prompt significantly. Consider truncating to a reasonable limit (e.g., 500–1000 chars) with a [truncated] indicator.

🟡 NIT — Attachment-only quotes (no text content) silently pass through as empty. Consider adding a placeholder like [attachment] so the agent knows a quote existed.


Review by 超渡法師 🙏 — rebase to unblock merge!

ChunHao-dev and others added 3 commits May 2, 2026 03:36
Quoted message content was injected into the prompt without running
resolve_mentions(), leaking raw Discord mention markup (<@BOT_ID>,
<@&ROLE_ID>) into the LLM prompt. Apply the same resolve_mentions()
pass used for the user's own message content.
Quoted messages longer than 1500 bytes are now truncated with a
'… [truncated]' marker to prevent prompt bloat. Uses a UTF-8-safe
truncation helper that respects char boundaries.

Addresses reviewer feedback on openabdev#527.
Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE ✅ — All previous concerns addressed. Clean, focused feature ready to merge.

📋 Baseline Check (Step 0)

Checked main branch: no reply/quote handling exists. Feature is 100% net-new. No overlap with recent merges.

🧭 Four-Question Framework

1. What problem does this solve?

When users reply to a message in Discord, the bot loses the quoted message context. Prompts like "summarize this" become ambiguous because the agent has no idea what "this" refers to.

2. How does it solve it?

Three new functions in src/discord.rs (+108 lines):

  • resolve_referenced_message() — async, prefers gateway-provided referenced_message (zero cost), falls back to HTTP API
  • format_quote_context() — pure formatting, prepends [Quoted message from @author] block
  • truncate_utf8() — UTF-8 safe truncation at 1500 bytes with … [truncated] marker

Inserted after resolve_mentions(), before the empty-check gate. Non-reply messages completely unaffected.

3. What was considered?

  • Gateway-first + HTTP fallback is the correct serenity 0.12 pattern
  • resolve_mentions() applied to quoted content too — prevents raw <@UID> leaking into prompt
  • Graceful degradation: HTTP fallback failure logs warning, continues without quote
  • Content length cap at 1500 bytes with UTF-8 char boundary safety

4. Is this the best approach?

Yes. Clean separation of async I/O and pure formatting. Well-tested (6 unit tests including UTF-8 boundary edge case). All 9 CI checks pass.

🚦 Traffic Light

🟢 INFO — Contributor addressed all previous review feedback:

  • ✅ Rebased against main — now MERGEABLE (was CONFLICTING)
  • ✅ Added 1500-byte truncation with UTF-8 safe boundary (was NIT in previous reviews)
  • resolve_mentions() applied to quoted content (CHC-Agent fix)
  • ✅ 6 unit tests with good edge case coverage (up from 4)
  • ✅ All 9 CI checks pass

🟢 INFOCow<str> usage in truncate_utf8() avoids unnecessary allocation when content is under the limit.

🟡 NIT — Attachment-only quotes (no text content) silently pass through as empty. A follow-up could add a placeholder like [attachment]. Non-blocking.


Review by 超渡法師 🙏

@chaodu-agent
Copy link
Copy Markdown
Collaborator

🟢 LGTM — Approve

Clean, well-scoped feature that fills a real gap. No concerns blocking merge.

1. What problem does it solve?

When a Discord user replies to (quotes) a message, the bot previously only forwarded the new message text. The agent had no idea what "this" or "that" referred to. This PR injects the quoted message content into the prompt so the agent gets full conversational context.

Closes #339.

2. How does it solve it?

Three new functions in src/discord.rs:

  • resolve_referenced_message() — async; prefers the gateway-provided referenced_message (zero-cost, already in memory), falls back to an HTTP API call via message_reference if the gateway didn't include the full message. Returns None for non-reply messages.
  • format_quote_context() — pure function; formats the quote block as [Quoted message from @author]:\n<content>\n\n<prompt>. Passes through unchanged if quoted content is empty.
  • truncate_utf8() — truncates to MAX_QUOTE_LENGTH (1500 bytes) on a valid UTF-8 char boundary using Cow to avoid allocation when no truncation is needed.

Injection point: right after resolve_mentions(), before the empty-prompt check. The quoted content also goes through resolve_mentions() to strip bot mentions consistently.

Purely additive: +116 lines, 0 deletions, single file.

3. What was considered?
  • Gateway-first strategy: avoids an HTTP round-trip in the common case (Discord usually includes referenced_message in gateway events). HTTP fallback handles the edge case where it's absent.
  • Truncation: 1500-byte cap prevents a single massive quoted message from blowing up the prompt/token budget.
  • UTF-8 safety: truncate_utf8 walks back to a valid char boundary — important for CJK and emoji-heavy Discord messages.
  • Non-reply messages unaffected: resolve_referenced_message returns None, so the existing code path is unchanged.
4. Is this the best approach?

Yes, this is the right approach for the scope of the problem:

  • The injection point (after resolve_mentions, before empty check) is the natural place.
  • Gateway-first with HTTP fallback is the standard serenity pattern.
  • The Cow-based truncation is efficient and correct.
  • 6 unit tests cover the formatting logic well, including edge cases (empty content, empty prompt, multiline, truncation, UTF-8 boundary).

No concerns about the design.

Traffic Light

🟢 INFO — Gateway-first strategy in resolve_referenced_message() is the right call; avoids unnecessary HTTP calls in the common case.

🟢 INFOresolve_mentions() is applied to the quoted content too, so bot mention stripping is consistent across both the quote and the user's prompt.

🟢 INFO — UTF-8-safe truncation with Cow is clean and avoids allocation in the no-truncation path.

🟢 INFO — Good test coverage: 6 tests covering normal, empty, multiline, truncation, and UTF-8 boundary cases.

🟡 NIT — The std::borrow::Cow import is added but only used by truncate_utf8. Consider whether truncate_utf8 could just return String to simplify — the Cow optimization only saves one allocation per non-truncated call, and this runs once per message. Not blocking; the current approach is technically correct and marginally more efficient.

🟡 NITMAX_QUOTE_LENGTH = 1500 is in bytes, not characters. For CJK-heavy content this could truncate at ~500 characters. Might be worth a comment clarifying the unit, or switching to char count if a more predictable UX is desired. Not blocking.


Reviewed by 超渡法師 🪬

@chaodu-agent
Copy link
Copy Markdown
Collaborator

Closing in favor of ADR: Context-Aware Token (#716)

Thanks for the clean implementation, @ChunHao-dev! During maintainer review, we discussed the broader design direction and concluded:

  1. Always-on quote prepending costs ~500 tokens per reply even when the agent already has the context from conversation history — most of the time, the agent knows what the user is referring to.
  2. The real need is broader: agents need the ability to actively fetch context on demand (historical messages, cross-channel info, thread metadata) — not just reply/quote content.
  3. This is better solved at the agent intelligence layer (agent decides when to fetch) rather than the transport layer (OAB always prepends).

We've opened PR #716 with an ADR proposing a context-aware token pattern that lets agents pull context when they judge it necessary. This supersedes the always-on approach in this PR.

Your work here directly informed the ADR design — the resolve_referenced_message() pattern and the gateway-first + HTTP fallback strategy are referenced as prior art.

Closing this PR in favor of #716. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: include Discord reply/quote context in agent prompt

6 participants